#! /usr/bin/env python
# encoding: utf-8

"""The Application class is responsible for handling the incoming
connections.
All Spyro's application will be based on this class.
"""

import logging
from contextlib import contextmanager
from importlib import import_module
# needed for debug uses
import imp
#import gc
# needed for the exception handling
import sys
import traceback


from spyro.config.build_register import build_register
from spyro.config.register import Register
from spyro.internal.utils.exceptions import NotFoundException

log = logging.getLogger(__name__)

class Application:
    """It's __init__ method takes in input the configuration file name,
    so create the Application object as follows:

    >>> from spyro.web import *
    >>> application = Application('myconfig.yaml')
    >>> # now use the application with any PEP3333-ready webserver.
    """

    def __init__(self, configname):
        build_register(configname)

    def __call__(self, environ, start_response):
        
        # on debug reload the configuration file making it avaiable for the
        # current request.
        if Register.debug:
            build_register(Register._fname)
    
        with self.parse_request(environ) as (controller, args):
            # this is needed because creating the response object will
            # delete some keys (via .pop() calls)
            # and if it was called a Response.not_found() we need to get
            # all the keys back again.
            wsgi_env = environ.copy()
            try:
                response, method = self.load_response(controller, environ)
                self.process_response(method, args)
                start_response(response.response._status, response.response._headers)
                # this try-catch is needed for the staticcontroller
                # if the content is already a bytes instance it has no encode()
                # methods.
                # XXX: this is not done in the exception handler because the
                # staticcontroller is for development only, but if there will
                # be any need to extend the support for bytes instances it will
                # be added.
                try:
                    return [response.response._body.encode()]
                except AttributeError:
                    return [response.response._body]
            except NotFoundException:
                # this is only raised by a controller calling the
                # Response.not_found() because if the requested url was not
                # found it is handled by the Application.load_response that
                # instead to raise a 404 exception loads directly the 404
                # webpage.
                environ = wsgi_env.copy()
                response, method = self.load_response(Register.pages[404], environ)
                self.process_response(method, None) # no arg is needed here
                start_response(response.response._status, response.response._headers)
                return [response.response._body.encode()]
            except Exception as ex:
                environ = wsgi_env.copy()
                feedback, exception = self.get_traceback(ex)
                response = self.error_handler(feedback, exception, environ)
                start_response(response.response._status, response.response._headers)
                return [response.response._body.encode()]

    @contextmanager
    def parse_request(self, environ):
        """Returns a two objects containing all the needed informations about
        the current request.
        It returns: response, args.

        "response" is a controller descriptor.
        "args" are the URL arguments: named args are preferred over
        non-named ones. If there are no args at all None is returned as "args".
        """

        current_request = environ.get('PATH_INFO')

        for reobj, response in Register.routes.items():
            result = reobj.match(current_request)
            if result:
                args = result.groupdict() or result.groups() or None
                yield response, args
                break
        else:
            yield Register.pages[404], None

    def load_response(self, controller, environ):
        """It loads the response method after the analyzation of the
        controller-descriptor string.
        Returns a tuple containing the controller object and the response
        method.

        If the debug is True it will also reload the module each time.
        """

        package, method = controller.split(':')
        rdot = package.rfind('.')
        class_ = package[rdot+1:]
        module = package[:rdot]

        log.debug("serving: '%(module)s.%(class_)s:%(method)s'" % locals())

        loaded_module = import_module(module)
        if Register.debug:
            #gc.collect() # this forces the deletion of the loaded classes
            imp.reload(loaded_module)

        if hasattr(loaded_module, class_):
            loaded_class = getattr(loaded_module, class_)
        else:
            raise AttributeError("Could not load %(class_)r from %(module)r" %
                                 locals())

        if hasattr(loaded_class, method):
            # initializing the response object
            loaded_class = loaded_class(environ)
            response = getattr(loaded_class, method)
        else:
            raise AttributeError("Could not load the method %(method)r" %
                                 locals())

        return (loaded_class, response)

    def process_response(self, method, args):
        """Calls the response "method" passing the "args" if any.

        Named arguments will be passed as keyword arguments, non named ones
        will be given as positional arguments.
        Note: the priority of the if-statement does not reflects the priority
        of the arguments as defined by parse_request. Named arguments have
        the highest priority, then come non-named ones and then no arguments.
        """

        # calling the method will modify the "response" object
        if args is None:
            method() # no args
        elif isinstance(args, tuple):
            method(*args) # non-named args
        else:
            method(**args) # named args

    def get_traceback(self, exception):
        """Generates the HTML traceback by analyzing the exception.
        It also creates a new exception message prepending the exception type
        to print it in the terminal.
        """

        tb = traceback.extract_tb(exception.__traceback__)
        exc_type = sys.exc_info()[0].__name__  # exception type

        # html entities
        html_tab = "&nbsp;" * 4

        calls = "".join(["File %r, Line %r, in %s<br>%s%s<br>" %
                         (call[0], call[1], call[2], html_tab, call[3])
                         for call in tb])

        feedback = """<div style="font-family: courier; font-size: 14px;">
        <strong>Traceback (most recent call last):</strong><br>
        %s
        <br><strong>%s</strong>: %s<br><br>
        <i>You see this traceback because your debug flag is True.</i>
        </div>""" % (calls, exc_type, str(exception))

        exception = exc_type + ': ' + str(exception)

        return (feedback, exception)

    def error_handler(self, feedback, exception, environ):
        """It handles the error condition calling the 500 error page.
        """

        log.error(exception)

        response, method = self.load_response(Register.pages[500], environ)

        if Register.debug:
            self.process_response(method, {'error':feedback})
        else:
            self.process_response(method, None)

        return response

    def make_request(self, environ):
        """For test purposes only.
        It makes a request, emulating a client.
        The path has to be defined in the environ dict under the key: PATH_INFO

        Returns a Storage object, follows a list of attributes:
            * status: status code
            * headers: header list
            * body: the html body
        """

        class Storage():
            pass

        response = Storage()

        def start_response(status, headers):
            response.status = status
            response.headers = headers

        response.body = "".join(element.decode()
                        for element in self.__call__(environ, start_response))

        return response
